package db

import (
	"bufio"
	"bytes"
	"code.google.com/p/marius/simplenote"
	"encoding/gob"
	"io/ioutil"
	"log"
	"os"
	"sync"
	"syscall"
)

// on-disk record (note)
type record struct {
	*simplenote.Note
	version     int
	syncVersion int
}

type Db struct {
	Path string
	sn   *simplenote.Simplenote
	sync.Mutex
}

type Note struct {
	index int
	text  string
	db    *Db
}

type fetchResult struct {
	key  string
	note *simplenote.Note
	err  error
}

var (
	nfetchers = 5
)

func openFile(path string) (*os.File, error) {
	file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return nil, err
	}

	if err := syscall.Flock(int(file.Fd()), 2|4); err != nil {
		file.Close()
		return nil, err
	}

	return file, nil
}

func closeFile(file *os.File) error {
	if err := syscall.Flock(int(file.Fd()), 8); err != nil {
		file.Close()
		return err
	}

	return file.Close()
}

func readRecords(file *os.File) ([]*record, error) {
	info, err := file.Stat()
	if err != nil {
		return nil, err
	}

	if info.Size() == 0 {
		return make([]*record, 0), nil
	}

	fileBytes, err := ioutil.ReadAll(file)
	if err != nil {
		return nil, err
	}

	buf := bytes.NewBuffer(fileBytes)
	dec := gob.NewDecoder(buf)
	var rs []*record
	if err := dec.Decode(&rs); err != nil {
		return nil, err
	}

	return rs, nil
}

func writeRecords(file *os.File, records []*record) error {
	if err := file.Truncate(0); err != nil {
		return err
	}

	if _, err := file.Seek(0, 0); err != nil {
		return err
	}

	wr := bufio.NewWriter(file)
	enc := gob.NewEncoder(wr)
	if err := enc.Encode(records); err != nil {
		return err
	}

	return wr.Flush()
}

func fetchMany(sn *simplenote.Simplenote, keys []string) []*fetchResult {
	keych := make(chan string, len(keys))
	resch := make(chan *fetchResult)
	for i := 0; i < nfetchers; i++ {
		go func() {
			for key := range keych {
				note, err := sn.Note(key)
				log.Printf("fetch %s", key)
				resch <- &fetchResult{key, note, err}
			}
		}()
	}

	for _, key := range keys {
		keych <- key
	}
	close(keych)

	res := []*fetchResult{}
	for i := 0; i < len(keys); i++ {
		res = append(res, <-resch)
	}

	return res
}

func saveNotes(sn *simplenote.Simplenote, notes []*simplenote.Note) ([]*simplenote.Note, error) {
	newNotes := []*simplenote.Note{}
	for i, n := range notes {
		newNote, err := sn.Save(n)
		if err != nil {
			log.Printf("Error saving note %d: %s", i, err)
			continue
		}

		if newNote.Content == "" {
			newNote.Content = n.Content
		}

		newNotes = append(newNotes, newNote)
	}

	return newNotes, nil
}

func updateNotes(sn *simplenote.Simplenote, notes []*simplenote.Note) ([]*simplenote.Note, []*simplenote.Note, error) {
	idx, err := sn.Index()
	if err != nil {
		return nil, nil, err
	}

	locals := make(map[string]*simplenote.Note)
	for _, n := range notes {
		// This can happen if the note failed to push
		// for any reason. We ignore it.
		if n.Key == "" {
			continue
		}

		locals[n.Key] = n
	}

	tofetch := []string{}
	for _, remote := range idx {
		local := locals[remote.Key]
		if local == nil || local.Syncnum < remote.Syncnum {
			tofetch = append(tofetch, remote.Key)
		}
	}

	fetched := fetchMany(sn, tofetch)

	updatedNotes := []*simplenote.Note{}
	newNotes := []*simplenote.Note{}

	for _, res := range fetched {
		if res.err != nil {
			log.Printf("Failed to fetch note %s", res.key)
			continue
		}

		local := locals[res.key]
		if local == nil {
			newNotes = append(newNotes, res.note)
		} else {
			updatedNotes = append(updatedNotes, res.note)
		}
	}

	return updatedNotes, newNotes, nil
}

func (db *Db) Sync() error {
	sn, err := db.login()
	if err != nil {
		return err
	}

	f, err := openFile(db.Path)
	if err != nil {
		return err
	}
	recs, err := readRecords(f)
	if err != nil {
		return err
	}
	closeFile(f)

	dirtyNotes := []*simplenote.Note{}
	for _, r := range recs {
		if r.syncVersion < r.version {
			dirtyNotes = append(dirtyNotes, r.Note)
		}
	}

	savedNotes, err := saveNotes(sn, dirtyNotes)
	if err != nil {
		return err
	}

	updatedMap := make(map[string]*simplenote.Note)
	for _, n := range savedNotes {
		updatedMap[n.Key] = n
	}

	toUpdate := []*simplenote.Note{}
	for _, r := range recs {
		if updatedMap[r.Key] == nil {
			toUpdate = append(toUpdate, r.Note)
		}
	}

	updatedNotes, newNotes, err := updateNotes(sn, toUpdate)
	if err != nil {
		return err
	}

	for _, n := range updatedNotes {
		updatedMap[n.Key] = n
	}

	f, err = openFile(db.Path)
	if err != nil {
		return err
	}
	defer closeFile(f)
	newRecs, err := readRecords(f)
	if err != nil {
		return err
	}

	for i := range recs {
		r := newRecs[i]

		if r.Key == "" {
			continue
		}
		updated := updatedMap[r.Key]
		if updated == nil || recs[i].version < newRecs[i].version {
			continue
		}

		r.Note = updated
		r.syncVersion = r.version
	}

	for _, n := range newNotes {
		newRecs = append(newRecs, &record{Note: n})
	}

	if err := writeRecords(f, newRecs); err != nil {
		return err
	}

	return nil
}

func (db *Db) login() (*simplenote.Simplenote, error) {
	db.Lock()
	defer db.Unlock()

	if db.sn == nil {
		sn, err := simplenote.Login()
		if err != nil {
			return nil, err
		}
		db.sn = sn
	}

	return db.sn, nil
}

/*
func (db *Db) Index() []*Note {
	db.Lock()
	defer db.Unlock()

	notes := make([]*Note, len(db.notes))
	for i := range db.notes {
		notes[i] = &Note{i, db.notes[i].Content, db}
	}

	return notes
}

func (db *Db) Commit(note *Note) error {
	db.Lock()
	defer db.Unlock()

	if note.db != db {
		d := new(dbNote)
		d.Content = note.text
		d.State = stateNew
		db.notes.add(d)
		note.index = d.Index
		return db.saveLocked()
	}


	return db.saveLocked()
}

func (db *Db) saveLocked() error {
	file, err := db.openFile()
	if err != nil {
		return err
	}
	defer db.closeFile(file)

	return writeNotes(file, db.notes)
}

func (db *Db) DebugString(simplenote *simplenote.Note) string {
	db.Lock()
	defer db.Unlock()

	note := db.notes.find(simplenote.Key)
	if note == nil {
		return "<notfound>"
	}

	var state string
	switch note.State {
	case stateNew:
		state = "new"
	case stateDirty:
		state = "dirty"
	default:
		state = "clean"
	}

	return fmt.Sprintf("Note: %s\nState: %s\nData:\n%s\n", simplenote.Key, state, simplenote.DebugString())
}

func (notes dbNotes) find(key string) *dbNote {
	if key == "" {
		return nil
	}

	for _, note := range notes {
		if note.Key == key {
			return note
		}
	}

	return nil
}

func (notes *dbNotes) add(note *dbNote) {
	note.Index = len(*notes)
	*notes = append(*notes, note)
}

func (notes dbNotes) get(i int) *dbNote {
	if i < len(notes) {
		return notes[i]
	}

	return nil
}
*/
